Skip to main content

Procedural Macros

Procedural macros are Rust functions that:

  • Run at compile time
  • Take Rust code as input
  • Return new Rust code as output

Instead of pattern matching like macro_rules!, they operate on Rust’s syntax tree (AST), allowing you to analyze and transform code programmatically.

They are written in Rust and compiled as a special kind of crate.

Types of Procedural Macros

There are three kinds:

TypeSyntaxPurpose
Function-likemy_macro!(...)Like println!, but procedural
Derive#[derive(MyTrait)]Automatically implement traits
Attribute-like#[my_attr]Modify items like functions, structs, modules

Key Differences vs macro_rules!

Declarative (macro_rules!)Procedural
Pattern-basedAST-based
SimplerMuch more flexible
Written inside normal cratesMust be in a proc-macro crate
No external crates neededUsually use syn, quote, proc-macro2

How Procedural Macros Work (Conceptually)

  1. Rust compiler parses your code.
  2. When it sees a procedural macro, it:
    • Converts the input tokens into a TokenStream.
    • Calls your macro function.
    • Replaces the macro invocation with the returned tokens.
  3. Compilation continues with the expanded code.

Setting Up a Procedural Macro Crate

Procedural macros must be in their own crate:

cargo new my_macros --lib

Edit Cargo.toml:

[lib]
proc-macro = true

[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"

Example 1: A Custom derive Macro

Let’s create a #[derive(HelloMacro)] that adds a method.

Step 1: Define the trait (in a normal crate)

pub trait HelloMacro {
fn hello();
}

Step 2: Implement the procedural macro (in the proc-macro crate)

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Parse input into a syntax tree
let ast = parse_macro_input!(input as DeriveInput);
let name = ast.ident;

// Generate code
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};

TokenStream::from(expanded)
}

Step 3: Use it in another crate

use my_macros::HelloMacro;

#[derive(HelloMacro)]
struct Foo;

fn main() {
Foo::hello();
}

What gets generated:

impl HelloMacro for Foo {
fn hello() {
println!("Hello from Foo!");
}
}

Example 2: Attribute Macro

Let’s write an attribute macro that logs function entry and exit.

Macro crate:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_fn(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);

let name = &input.sig.ident;
let block = &input.block;
let vis = &input.vis;
let sig = &input.sig;

let expanded = quote! {
#vis #sig {
println!("Entering {}", stringify!(#name));
let result = (|| #block)();
println!("Exiting {}", stringify!(#name));
result
}
};

TokenStream::from(expanded)
}

Usage:

use my_macros::log_fn;

#[log_fn]
fn add(a: i32, b: i32) -> i32 {
a + b
}

fn main() {
let x = add(2, 3);
}

Expanded code:

fn add(a: i32, b: i32) -> i32 {
println!("Entering add");
let result = (|| { a + b })();
println!("Exiting add");
result
}

Example 3: Function-like Procedural Macro

use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
quote! {
fn answer() -> i32 {
42
}
}.into()
}

Usage:

make_answer!();

fn main() {
println!("{}", answer());
}

Why Use Procedural Macros

Use them when:

  • You need to analyze Rust syntax, not just match patterns.
  • You want to generate code based on struct fields, attributes, generics, lifetimes, etc.
  • You want to enforce compile-time invariants or build DSLs.

Common real-world uses:

  • serde’s #[derive(Serialize, Deserialize)]
  • tokio’s #[tokio::main]
  • thiserror’s #[derive(Error)]
  • clap’s #[derive(Parser)]

Safety and Best Practices

  • Procedural macros are powerful but complex — test them thoroughly.
  • Always provide good compile-time error messages using syn::Error.
  • Keep macro logic small and predictable.
  • Prefer macro_rules! if it can solve the problem — it’s simpler and faster to maintain.